Skip to content

FE-1545: Adopt the worksheet focus layer in the left sidebar lists and search - #9420

Open
kube wants to merge 1 commit into
claude/fe-1544-spreadsheet-focus-layerfrom
claude/fe-1545-sidebar-focus-layer
Open

FE-1545: Adopt the worksheet focus layer in the left sidebar lists and search#9420
kube wants to merge 1 commit into
claude/fe-1544-spreadsheet-focus-layerfrom
claude/fe-1545-sidebar-focus-layer

Conversation

@kube

@kube kube commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Stacked on #9418. Adopt the worksheet keyboard-flow layer from #9411 across the left sidebar. The sidebar carried three hand-rolled focus models: the filterable lists tracked a virtual focus index in React state over unfocusable rows with hover-only row menus, the nets list made every row and its invisible delete button a tab stop with no arrow keys, and search hopped focus between input and results through DOM queries while remounting the results on every keystroke.

🔗 Related links

🔍 What does this change?

  • The filterable lists (nodes, types, differential equations, parameters, entities tree) move to real DOM focus through useFocusStops: each list is one Tab stop, arrows move and select as they move, Shift+Arrow extends the range, and group headers collapse/expand with ArrowLeft/ArrowRight. The virtual focusedIndex state, the row-ref bookkeeping, its scroll effect, and a dead per-row key handler are deleted.
  • Row menus and group add buttons become keyboard-reachable: a new RowActionSlot registers the row's trailing button as its second column, so ArrowRight reaches it and ArrowLeft returns. The slots reveal on focus as well as hover.
  • The nets list folds onto the same model: one Tab stop, arrow keys, Enter/Space activates a net, Delete removes a subnet, and the delete button is reachable with ArrowRight — it was previously invisible-but-tabbable on every row.
  • Search joins the input and its results in one vertical FocusStack at the panel level: ArrowDown in the input hands focus to the list and ArrowUp from the first result hands it back, replacing the closest("[data-panel]")/querySelector focus hops. The key={query} remount of the results is gone, so the list no longer resets on every keystroke.
  • useFocusStops.tabIndexFor gains a fallback: when the remembered position's stop disappears (a row deleted or filtered away), the table's tab stop falls back to the first stop instead of vanishing from the tab order. Covered by a new layer test.
  • The orphaned clampIndex helper is deleted.
  • Intentional behaviour changes: focus highlight is now real :focus styling; Escape clears the selection via the list container (as before) and the editor's global Escape; the nets list's row highlight follows focus; list collapse state and click semantics (plain/Shift/Cmd click) are unchanged.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • Single-letter editor shortcuts (v/h/n/t) still fire while a list row holds focus, as they did before this PR. Settling that globally is a follow-up to the focus-layer adoption.
  • The section headers between the lists are not part of the arrow flow yet; that is the shared-chrome adoption (headers, tabs, toolbars) proposed separately.

🐾 Next steps

  • Shared chrome (SubViewHeader, tab bars, toolbars) is the next adoption target after this and FE-1544 (internal).

🛡 What tests cover this?

  • New filterable-list-sub-view.test.tsx: roving tabindex, select-as-you-move with Shift ranges, ArrowRight to the row menu and back, group collapse/expand with hidden children skipped.
  • New layer test in focus-flow.test.tsx for the tabIndexFor fallback after the remembered stop unmounts.
  • Existing focus-flow.test.tsx covers the movement contract.

❓ How to test this?

  1. yarn dev (or the website demo), open the left sidebar.
  2. Tab into a list (one stop), walk rows with arrows and watch the canvas selection follow; Shift+ArrowDown to range-select; ArrowRight to a row's menu and Enter to open it.
  3. In the entities tree, ArrowLeft/ArrowRight on the group headers, ArrowRight to a group's + button.
  4. In the Nets list (subnets enabled): arrows between nets, Enter to switch, Delete on a subnet row, ArrowRight to its delete button, double-click to rename.
  5. Cmd+F, type a query, ArrowDown into the results, walk them (selection follows), ArrowUp from the first result back into the input — and note the highlight no longer resets while typing.

The sidebar's visuals are unchanged apart from focus highlighting; existing docs screenshots stay valid.

🤖 Generated with Claude Code

@kube kube self-assigned this Aug 28, 2026
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hash Ready Ready Preview Sep 1, 2026 2:48pm UTC
petrinaut Ready Ready Preview Sep 1, 2026 2:48pm UTC
petrinaut-docs Ready Ready Preview Sep 1, 2026 2:48pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Sep 1, 2026 2:48pm UTC

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Aug 28, 2026
@kube
kube marked this pull request as ready for review August 28, 2026 20:47
@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Changes are localized to sidebar keyboard/focus UX and shared focus helpers; subnet delete still respects read-only, with new tests covering the main flows.

Overview
The left sidebar replaces three ad-hoc keyboard models with the shared worksheet focus stack (useFocusStops, FocusRoot / FocusStack, useFocusMember).

Filterable lists (nodes, types, equations, parameters, entity tree) now use one Tab stop per list with roving tabIndex, real :focus styling, and arrow-driven selection (including Shift ranges). Virtual focusedIndex state, clampIndex, and container-level arrow handling are removed. RowActionSlot wires trailing menus and group + buttons as a second focus column (ArrowRight / ArrowLeft), shown on hover and focus-within.

The nets list follows the same pattern: arrows between Root and subnets, Enter/Space to activate, Delete/Backspace to remove a subnet, and ArrowRight to the delete control (no longer a hidden tab stop on every row).

Search wraps the open-search UI in a vertical FocusStack so ArrowDown from the input enters results and ArrowUp from the first row returns; DOM querySelector hops and key={query} remounting of results are dropped so focus/highlight stay stable while typing.

useFocusStops.tabIndexFor falls back to the first stop when the remembered row disappears (e.g. delete or filter), with a new focus-flow test plus filterable-list-sub-view.test.tsx for list behavior. User-facing keyboard docs in drawing-a-net.md are updated; @hashintel/petrinaut gets a patch changeset.

Reviewed by Cursor Bugbot for commit 0e5cc51. Bugbot is set up for automated code reviews on this repo. Configure here.

row: { item: T; isGroup: boolean },
) => {
event.stopPropagation();
setFocusedIndex(index);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background click steals list keyboard focus

Medium Severity

The listbox container is still click-focusable via tabIndex={-1}, but arrow handling now lives only on the rows. Clicking empty space to clear the selection focuses the container, after which ArrowUp/ArrowDown do nothing until a row is focused again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 75fa218. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94a0e8c. Configure here.

@kube
kube force-pushed the claude/fe-1545-sidebar-focus-layer branch from 7858534 to 57dd11c Compare August 30, 2026 20:45
@kube
kube force-pushed the claude/fe-1545-sidebar-focus-layer branch from 57dd11c to 2906130 Compare August 30, 2026 21:43
@kube
kube force-pushed the claude/fe-1545-sidebar-focus-layer branch from 2906130 to 205d0dc Compare August 30, 2026 21:54
@kube
kube force-pushed the claude/fe-1545-sidebar-focus-layer branch from 205d0dc to 6035d63 Compare August 30, 2026 22:02
@kube
kube force-pushed the claude/fe-1545-sidebar-focus-layer branch from 6035d63 to 926847a Compare August 31, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant